/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.workflow.simple.converter.json;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.activiti.workflow.simple.definition.*;
import org.activiti.workflow.simple.definition.form.*;
import org.activiti.workflow.simple.exception.SimpleWorkflowException;

import java.io.InputStream;
import java.io.Writer;
import java.util.List;

/**
 * A converter capable of converting {@link WorkflowDefinition}s from and to a
 * JSON representation.
 * <br>
 * <p>Instances of this class <b>are thread-safe</b>.</p>
 *
 * @author Frederik Heremans
 */
public class SimpleWorkflowJsonConverter {

    protected ObjectMapper objectMapper;
    protected List<Class<?>> additionalModelClasses;

    /**
     * @param inputStream the stream to read the JSON from.
     * @return The workflow definition instance, read from the given input-stream.
     * @throws SimpleWorkflowException when an error occurs while reading or parsing the definition.
     */
    public WorkflowDefinition readWorkflowDefinition(InputStream inputStream) throws SimpleWorkflowException {
        try {
            return getObjectMapper().readValue(inputStream, WorkflowDefinition.class);
        } catch (Exception e) {
            throw wrapExceptionRead(e);
        }
    }

    /**
     * @param bytes array representing the definition JSON.
     * @return The workflow definition instance, parsed from the given array.
     * @throws SimpleWorkflowException when an error occurs while parsing the definition.
     */
    public WorkflowDefinition readWorkflowDefinition(byte[] bytes) throws SimpleWorkflowException {
        try {
            return getObjectMapper().readValue(bytes, WorkflowDefinition.class);
        } catch (Exception e) {
            throw wrapExceptionRead(e);
        }
    }

    public void writeWorkflowDefinition(WorkflowDefinition definition, Writer writer) {
        try {
            getObjectMapper().writeValue(writer, definition);
        } catch (Exception e) {
            throw wrapExceptionWrite(e);
        }
    }

    /**
     * @param inputStream the stream to read the JSON from.
     * @return The workflow definition instance, read from the given input-stream.
     * @throws SimpleWorkflowException when an error occurs while reading or parsing the definition.
     */
    public FormDefinition readFormDefinition(InputStream inputStream) {
        try {
            return getObjectMapper().readValue(inputStream, FormDefinition.class);
        } catch (Exception e) {
            throw wrapExceptionRead(e);
        }
    }

    public void writeFormDefinition(FormDefinition definition, Writer writer) {
        try {
            getObjectMapper().writeValue(writer, definition);
        } catch (Exception e) {
            throw wrapExceptionWrite(e);
        }
    }

    /**
     * @param e exception to wrap
     * @return an {@link SimpleWorkflowException} to throw, wrapping the given exception.
     */
    protected SimpleWorkflowException wrapExceptionRead(Exception e) {
        return new SimpleWorkflowException("Error while parsing JSON", e);
    }

    /**
     * @param e exception to wrap
     * @return an {@link SimpleWorkflowException} to throw, wrapping the given exception.
     */
    protected SimpleWorkflowException wrapExceptionWrite(Exception e) {
        return new SimpleWorkflowException("Error while writing JSON", e);
    }

    protected ObjectMapper getObjectMapper() {
        if (objectMapper == null) {

            // Ensure ObjectMapper is only initialized once
            synchronized (this) {
                if (objectMapper == null) {
                    objectMapper = new ObjectMapper();

                    // Register all property-definition model classes as sub-types
                    objectMapper.registerSubtypes(ListPropertyDefinition.class, TextPropertyDefinition.class,
                            ReferencePropertyDefinition.class, DatePropertyDefinition.class, NumberPropertyDefinition.class, BooleanPropertyDefinition.class);

                    // Register all step-types
                    objectMapper.registerSubtypes(HumanStepDefinition.class, FeedbackStepDefinition.class, ParallelStepsDefinition.class, ChoiceStepsDefinition.class,
                            ListStepDefinition.class, ListConditionStepDefinition.class, ScriptStepDefinition.class, DelayStepDefinition.class);
                    // Register additional sub-types to allow custom model entities to be
                    // deserialized correctly
                    if (additionalModelClasses != null) {
                        objectMapper.registerSubtypes(additionalModelClasses.toArray(new Class<?>[]{}));
                    }
                }
            }
        }
        return objectMapper;
    }
}
